#
# openlookup/engine/multivalued.py
#
# Copyright 2008-2009 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import time
from os import urandom
from hashlib import sha1
from binascii import hexlify

from openlookup.engine.item import create_multivalued_item
from openlookup.engine.db import DB

MAX_MAXVALS = 20

class MultivaluedEngine(object):
    def __init__(self, filename, create=False, fast=False):
        self.db = DB(filename, create=create, fast=fast)

    def __enter__(self): return self
    def __exit__(self, *exc_info): self.close()
    def close(self): self.db.close()

    def query(self, key, maxvals=MAX_MAXVALS, placemark=''):
        """
        Fetch some values for a key.

        The result is a list of at most maxvals tuples and a placemark
        (cursor) used to request more.  The elements of each tuple are
        the value, time-to-live, and secret hash (empty if the value
        is not removable).

        If the returned placemark is empty, there are no more values.
        If it is not empty, there may be more.

        Distinct values are returned in a deterministic pseudo-random
        circular order, starting from a randomly chosen value (but not
        guaranteeing that all values are equally likely to be chosen).

        More specifically, the values are distributed randomly on a
        ring and returned starting from a random point on the ring.
        Identical values with different secret hashes are located very
        near each other on the ring.
        """
        maxvals = max(1, min(MAX_MAXVALS, int(maxvals)))
        key_hash = sha1(key).hexdigest()

        assert len(placemark) in [0, 120, 160]
        if placemark:
            anchor, mark = placemark[:80], placemark[80:]
        else:
            anchor = mark = hexlify(urandom(40))

        results = []
        for item in self.db.query(key_hash, mark, anchor):
            if item.type != 'multivalued':
                continue

            ttl = item.get_ttl()
            if ttl < 0:
                continue

            results.append((item.value, ttl, item.secret_hash))
            if len(results) == maxvals:
                lastmark = item.value_hash + item.secret_hash
                if lastmark == anchor:
                    break   # ridiculous border case
                return [results, anchor + lastmark]

        return [results, '']

    def put(self, key, value, ttl=None, secret_hash=''):
        """
        Store an additional value under the specified key.

        The time-to-live for the value defaults to the maximum.  If a
        secret hash is provided, the value is removable.

        If the key, value, and (possibly empty) secret hash match an
        existing record, the time-to-live of the record is updated.
        """
        key_hash = sha1(key).hexdigest()
        value_hash = sha1(value).hexdigest()
        item = create_multivalued_item(key_hash, value_hash, secret_hash,
                                       value, None, ttl)
        self.put_item(item)

    def rm(self, key, value_hash, secret, ttl=None):
        """
        Remove a value.

        The specified time-to-live should be at least as long as the
        remaining time-to-live.  It may be increased automatically to
        that value, but this cannot be guaranteed.  If not specified,
        the time-to-live defaults to the maximum.
        """
        if len(value_hash) != 40:
            raise ValueError('Bad value hash.')

        key_hash = sha1(key).hexdigest()
        secret_hash = sha1(secret).hexdigest()
        item = create_multivalued_item(key_hash, value_hash, secret_hash,
                                       None, secret, ttl)
        self.put_item(item)

    def put_item(self, item):
        item.validate()
        self.db.update(item)
